package com.xiaomaoguai.fcp.pre.kepler.mock.configuration;

import com.xiaomaoguai.fcp.pre.kepler.mock.dc.DCMockHandler;
import com.xiaomaoguai.fcp.pre.kepler.mock.dc.FeignMockClientGenerator;
import com.xiaomaoguai.fcp.pre.kepler.mock.properties.FeignMockConstants;
import com.xiaomaoguai.fcp.pre.kepler.mock.properties.FeignMockProperties;
import com.xiaomaoguai.fcp.pre.kepler.mock.spi.GlueMockExecutionHandler;
import com.xiaomaoguai.fcp.pre.kepler.mock.spi.MockExecutionHandler;
import com.xiaomaoguai.fcp.pre.kepler.mock.utils.FeignClassInfoHolder;
import com.xiaomaoguai.fcp.pre.kepler.mock.utils.FeignMethodInvocation;
import com.xiaomaoguai.fcp.pre.kepler.common.autoconfiguration.KeplerConfigAutoConfiguration;
import com.xiaomaoguai.fcp.pre.kepler.common.config.listener.glue.GlueConfigurator;
import feign.Logger;
import javassist.CtClass;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition;
import org.springframework.web.servlet.mvc.condition.MediaTypeExpression;
import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.util.Set;

/**
 * @author August.Zhang
 * @version v1.0.0
 * @date 2020/2/14 14:11
 * @since JDK 1.8
 */
@Slf4j
@Configuration
@EnableConfigurationProperties(FeignMockProperties.class)
@ConditionalOnProperty(value = FeignMockConstants.FEIGN_MOCK_ENABLE, havingValue = "true", matchIfMissing = false)
@AutoConfigureAfter({WebMvcAutoConfiguration.class, KeplerConfigAutoConfiguration.class})
public class FeignMockAutoConfiguration {

	@Configuration
	static class FeignMockDependencyClassConfiguration {

		@Bean
		@ConditionalOnMissingBean
		public Logger.Level feignLoggerLevel() {
			return Logger.Level.FULL;
		}

		@Bean
		@ConditionalOnMissingBean
		public MockExecutionHandler mockExecutionHandler(GlueConfigurator glueConfigurator) {
			return new GlueMockExecutionHandler(glueConfigurator);
		}

	}

	@Autowired
	private RequestMappingHandlerMapping requestMappingHandlerMapping;

	@Autowired
	private AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor;

	@Autowired
	private DefaultListableBeanFactory defaultListableBeanFactory;

	@Autowired
	private FeignMockProperties feignMockProperties;

	@Autowired
	private MockExecutionHandler mockExecutionHandler;

	@PostConstruct
	public void init() {
		Set<Class<?>> feignClasses = FeignClassInfoHolder.getFeignClasses();
		for (final Class<?> feignClass : feignClasses) {
			Method[] methods = feignClass.getMethods();
			for (final Method method : methods) {
				if (method.isAnnotationPresent(RequestMapping.class) && !FeignClassInfoHolder.hasMapped(method)) {
					//方法调用信息赋值到类中
					FeignMethodInvocation feignMethodInvocation = new FeignMethodInvocation();
					try {
						CtClass feignMethodImplClass = FeignMockClientGenerator.createClassMethod(feignClass, method, feignMethodInvocation, feignMockProperties);
						if (feignMethodImplClass != null) {
							log.debug("====> create Feign Mock Handler Class :{}", feignMethodImplClass.getName());
							registrar(feignMethodImplClass, feignMethodInvocation, method);
							FeignClassInfoHolder.addFeignMethod(method);
						}
					} catch (Exception e) {
						log.error("create Feign Mock Handler Class error", e);
					}
				}
			}
		}
	}

	/**
	 * 注册 requestMapping
	 *
	 * @param ctClass               ct类
	 * @param feignMethodInvocation 方法调用
	 * @param method                方法
	 * @throws Exception 异常
	 */
	public void registrar(CtClass ctClass, FeignMethodInvocation feignMethodInvocation, Method method) throws Exception {
		debug(ctClass);

		Class<?> classClass = ctClass.toClass();
		Object mockHandlerObject = classClass.newInstance();
		DCMockHandler dcMockHandler = (DCMockHandler) mockHandlerObject;
		dcMockHandler.setFeignMethodInvocation(feignMethodInvocation);
		dcMockHandler.setMockExecutionHandler(mockExecutionHandler);

		RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
		String[] paths = getRequestPaths(requestMapping);

		PatternsRequestCondition patternsRequestCondition = new PatternsRequestCondition(paths);
		RequestMethodsRequestCondition requestMethodsRequestCondition = new RequestMethodsRequestCondition();
		ParamsRequestCondition paramsRequestCondition = new ParamsRequestCondition();
		HeadersRequestCondition headersRequestCondition = new HeadersRequestCondition();
		ConsumesRequestCondition consumesRequestCondition = new ConsumesRequestCondition();
		ProducesRequestCondition producesRequestCondition = new ProducesRequestCondition();
		MediaTypeExpression mediaTypeExpression = new MediaTypeExpression() {

			@Override
			public MediaType getMediaType() {
				return MediaType.APPLICATION_JSON_UTF8;
			}

			@Override
			public boolean isNegated() {
				return false;
			}
		};
		producesRequestCondition.getExpressions().add(mediaTypeExpression);

		String ctClassName = ctClass.getName();
		RequestMappingInfo requestMappingInfo = new RequestMappingInfo(ctClassName + paths[0], patternsRequestCondition, requestMethodsRequestCondition, paramsRequestCondition, headersRequestCondition, consumesRequestCondition, producesRequestCondition, null);
		autowiredAnnotationBeanPostProcessor.processInjection(dcMockHandler);

		BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(DCMockHandler.class);
		defaultListableBeanFactory.registerBeanDefinition(ctClassName, beanDefinitionBuilder.getBeanDefinition());

		Class<? extends RequestMappingHandlerMapping> clazz = requestMappingHandlerMapping.getClass();
		Class<?> superclass = clazz.getSuperclass().getSuperclass();
		Method detectHandlerMethods = superclass.getDeclaredMethod("detectHandlerMethods", Object.class);
		detectHandlerMethods.setAccessible(true);
		detectHandlerMethods.invoke(requestMappingHandlerMapping, ctClassName);

		String methodName = method.getName();
		Class<?>[] parameterTypes = method.getParameterTypes();
		requestMappingHandlerMapping.registerMapping(requestMappingInfo, dcMockHandler, dcMockHandler.getClass().getMethod(methodName, parameterTypes));
	}

	/**
	 * get请求路径
	 *
	 * @param requestMapping 请求映射
	 * @return {@link String[]}
	 */
	private String[] getRequestPaths(final RequestMapping requestMapping) {
		String[] paths = requestMapping.path();
		String[] value = requestMapping.value();
		return ArrayUtils.addAll(paths, value);
	}

	private void debug(CtClass ctClass) throws Exception {
		String debugClassPath = feignMockProperties.getDebugClassPath();
		if (feignMockProperties.isDebug() && StringUtils.isNotBlank(debugClassPath)) {
			ctClass.writeFile(debugClassPath);
		}
	}

}
